import fg from 'fast-glob';
import fs from 'fs-extra';
import path from 'path';
import md5 from 'md5';
import xmljs from 'xml-js';
import { count, findFiles, getValues, joinValues } from "../tools/vendor.js";
import { AttrDef, CfgLineDef, CfgXmlSet, ComplexAttrDef, EntryDef, JSONDropFieldRule, JsonDef, JsonDiffValueRule, NumStr, StructDef } from '../typings';
import { isArrayEntry, isComplexType, isLiteralStringType, name_ClientUsed, NumberLike, StringLike } from './def.js';
import { JsonStructParser } from './JsonStructParser.js';
import { SvnHolder, adjustSVN } from './SvnHolder.js';
import sizeof from 'object-sizeof';
import convertSize from 'convert-size';
import { Alert } from '../tools/alert.js';
import _ from 'lodash';
import { toolchain } from '../tools/toolchain.js';

declare type JsonOut = {
    o: any, 
    isNotUsed: boolean
}

declare type TToDelete = {
    o: any,
    deleteKeys: string[]
}

export class Xml2Json {

    private static ins: Xml2Json;
    public static get Instance(): Xml2Json {
        if(!Xml2Json.ins) Xml2Json.ins = new Xml2Json();
        return Xml2Json.ins;
    }

    private readonly HanPattern = /[\u4e00-\u9fa5]+/;

    private structPath!: string;
    private xmlPath!: string;
    private jsonPath!: string;
    private errorIdPath!: string;

    private defMd5Map: { [structName: string]: string } = {};

    private oldMd5Map?: { [file: string]: string };
    private newMd5Map: { [file: string]: string } = {};

    private nameMap?: { [xmlName: string]: string};

    private strings: string[] = [''];
    private allLangs: string[] = [];

    private crtXml!: string;
    private crtStruct!: string;
    private crtWeight: number = 0;

    private toDeletes: TToDelete[] = [];
    private errorMap: { [file: string]: string[] } = {};

    public async execute(): Promise<void> {
        if (toolchain.projCfg.xml2json?.nameMap) {
            this.nameMap = JSON.parse(toolchain.projCfg.xml2json.nameMap);
        }
        const tmpFolder = toolchain.options.publish ? toolchain.options.project + '_publish' : toolchain.options.project;
        const temp = path.join(toolchain.options.workspace!, tmpFolder);
        this.structPath = path.join(temp, 'struct');
        this.xmlPath = path.join(temp, 'xml');
        this.jsonPath = path.join(temp, 'json');
        this.errorIdPath = path.join(temp, 'ts/protocol');
        const defMd5Path = path.join(temp, 'defMd5.json');
        const md5Path = path.join(temp, 'md5.json');
        // 更新svn
        await SvnHolder.Instance.checkSVN(!toolchain.options.debug, this.structPath, adjustSVN(toolchain.projCfg.Svn.DesignerSvn) + 'xml');
        await SvnHolder.Instance.checkSVN(!toolchain.options.debug, this.xmlPath, adjustSVN(toolchain.projCfg.Svn.DesignerSvn) + 'bin/Client');
        if (toolchain.projCfg.Project.ProjectType === 'H5ts') {
            await SvnHolder.Instance.checkSVN(true, this.jsonPath, adjustSVN(toolchain.projCfg.Svn.ClientSvn) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/bin/res/data'));
            await SvnHolder.Instance.checkSVN(true, this.errorIdPath, adjustSVN(toolchain.projCfg.Svn.ClientSvn) + (toolchain.projCfg.xml2protocol?.protocolRoot ?? 'project/src/automatic/protocol'));
        } else {
            await SvnHolder.Instance.checkSVN(true, this.jsonPath, adjustSVN(toolchain.projCfg.Svn.ClientSvn) + (toolchain.projCfg.xml2json?.jsonRoot ?? 'project/Assets/AssetSources/data'));
            await SvnHolder.Instance.checkSVN(true, this.errorIdPath, adjustSVN(toolchain.projCfg.Svn.ClientSvn) + (toolchain.projCfg.xml2protocol?.protocolRoot ?? 'project/TsScripts/System/protocol'));
        }
        // 读入md5信息
        if(fs.existsSync(defMd5Path)) {
            this.defMd5Map = await fs.readJson(defMd5Path);
        } else {
            console.log("No def md5 record found.");
        }
        if(fs.existsSync(md5Path)) {
            this.oldMd5Map = await fs.readJson(md5Path);
        } else {
            console.log("No old md5 record found.");
        }

        await JsonStructParser.Instance.readStructs(this.structPath, true);

        // 调试模式下先删除就的所有json文件
        if (toolchain.options.force) {
            const files = await fs.readdir(this.jsonPath);
            for (const f of files) {
                if (f.endsWith('.json')) {
                    await fs.remove(path.join(this.jsonPath, f));
                }
            }
        }
        
        // 转json
        await this.convert();
        // 生成ErrorId.ts
        await this.makeErrorId();

        // 写md5信息
        await fs.writeJSON(defMd5Path, this.defMd5Map, { spaces: 2 });
        await fs.writeJSON(md5Path, this.newMd5Map, { spaces: 2 });

        // 提交svn
        if(!toolchain.options.debug) {
            console.log("Committing...");
            await SvnHolder.Instance.commitSVN('xml2json@fy-convertor~', this.jsonPath, this.errorIdPath);
        } else {
            console.log("Debug mode, committing skipped.");
        }

        // 统计json占用内存
        const files = await fs.readdir(this.jsonPath);
        const sizeDetails: { name: string, size: number }[] = [];
        let totalSize: number = 0;
        for (const f of files) {
            if (f.endsWith('.json')) {
                const obj = await fs.readJSON(path.join(this.jsonPath, f));
                const s = sizeof(obj);
                sizeDetails.push({ name: f, size: s });
                totalSize += s;
            }
        }
        sizeDetails.sort((a, b) => b.size - a.size);
        console.log('total json size:', convertSize(totalSize, { accuracy: 1 }));
        console.log('-----------------');
        for (const sd of sizeDetails) {
            console.log(sd.name + ':', convertSize(sd.size, { accuracy: 1 }));
        }
    }

    private async convert(): Promise<void> {
        // 获取drop字段配置
        const dfRuleMap: Record<string, JSONDropFieldRule> = {};
        // 解析diff规则
        const dfRules = toolchain.projCfg.xml2json?.dropFieldRules;
        if (dfRules) {
            for (const rule of dfRules) {
                dfRuleMap[rule.name] = rule;
            }
        }

        // 获取diff配置
        const dvRuleMap: Record<string, JsonDiffValueRule> = {};
        // 解析diff规则
        const dvRules = toolchain.projCfg.xml2json?.diffValueRules;
        if (dvRules) {
            for (const rule of dvRules) {
                dvRuleMap[rule.name] = rule;
            }
        }

        const files = findFiles(this.xmlPath, ['.xml'], '+');
        const structJsonMap: { [structName: string]: { cfgs: any[], fb: string }[]} = {};
        const structXmlMap: { [structName: string]: string[] } = {};
        for(let f of files) {
            this.crtXml = f;
            // if(!f.endsWith('PinstanceDiffBonus.xml')) continue;
            if(f.endsWith('language.xml') || f.endsWith('ErrnoLang.config.xml')) continue;

            let fileContent = await fs.readFile(f, 'utf-8');

            const xml = xmljs.xml2json(fileContent, {compact: true, spaces: 0});
            const doc = JSON.parse(xml) as CfgXmlSet;

            const structNameKey = this.identifyStructNameKey(doc);
            if(!structNameKey) {
                console.error(`[ERROR]Cannot identify struct name: ${f}`);
                continue;
            }
            
            const fb = path.basename(f);
            const rawStructName = structNameKey.substring(0, structNameKey.length - 4);
            const structName = rawStructName.replace('_Flash', 'M');
            this.crtStruct = structName;
            // if (structName != 'PinGuanQiaM') continue;
            let xmls: string[] = structXmlMap[structName];
            if (!xmls) {
                structXmlMap[structName] = xmls = [];
            }
            xmls.push(fb);

            const structDef = JsonStructParser.Instance.getStructDef(structName);
            if(!structDef) {
                continue;
            }
            const dvRule = dvRuleMap[structName];

            // 计算定义md5
            const defMd5 = md5(this.getStructFingerprint(structDef) + (dvRule ? JSON.stringify(dvRule) : ''));
            const oldDefMd5 = this.defMd5Map[structName];
            this.defMd5Map[structName] = defMd5;
            
            const fileMd5 = md5(fileContent);
            this.newMd5Map[fb] = fileMd5;
            if(oldDefMd5 === defMd5 && this.oldMd5Map && this.oldMd5Map[fb] == fileMd5 && !toolchain.options.force) {
                console.log(`MD5 no change, skip: ${fb}`);
                continue;
            }
            console.log(`converting json: ${fb}`);

            const onlyForLang = toolchain.projCfg.xml2json?.onlyForLang?.includes(fb);
            
            const cfgObjs = doc[structNameKey][rawStructName];

            let cfgs: JsonDef[] = [];

            let rawCfgs: CfgLineDef[];
            if(cfgObjs instanceof Array) {
                rawCfgs = cfgObjs;
            } else {
                rawCfgs = [cfgObjs];
            }
            for(let i = 0, len = rawCfgs.length; i < len; i++) {
                this.crtWeight = 0;
                const c = rawCfgs[i];
                const cu = c[name_ClientUsed];
                if (cu != null) {
                    if ((cu as AttrDef)._text === '0 ') {
                        continue;
                    }
                }
                let out = await this.structToJson(c, structDef, onlyForLang ?? false);
                if(!out.isNotUsed && this.crtWeight > 0) {
                    cfgs.push(out.o);
                }
            }

            if (cfgs.length === rawCfgs.length) {
                // 那就把第一个当成表头
                cfgs.shift();
            }

            // 检查表格合法性
            await this.checkValidity(structDef, cfgs);

            if (!onlyForLang) {
                // 增强版排序，需要先排序，再删除optional字段
                if (structDef._attributes.sortPlusKeys) {
                    const rawSkeys = structDef._attributes.sortPlusKeys.split(',');
                    const skeys = rawSkeys.map((v) => v.replace(/[\+-]+$/, ''));
                    const orders = rawSkeys.map((v) => v.endsWith('-') ? 'desc' : 'asc');
                    cfgs = _.orderBy(cfgs, skeys, orders);
                }
                // drop字段
                const dfRule = dfRuleMap[structName];
                if (dfRule) this.dropFields(dfRule, cfgs, structDef);
                // diff数据
                const dvRule = dvRuleMap[structName];
                if (dvRule) this.diffValues(dvRule, cfgs, structDef);

                // 最后删除optional字段，因上述各种处理可能涉及到排序，而排序可能使用到optional字段
                for (const td of this.toDeletes) {
                    for (const k of td.deleteKeys) {
                        delete td.o[k];
                    }
                }
                this.toDeletes.length = 0;

                const info = { cfgs, fb };
                let jinfos = structJsonMap[structName];
                if (jinfos == null) {
                    structJsonMap[structName] = jinfos = [info];
                } else {
                    jinfos.push(info);
                }
            }
        }

        // 检查是否有错误
        const errorFiles: string[] = [];
        for (const file in this.errorMap) errorFiles.push(file);
        if (errorFiles.length > 0) {
            for (const file of errorFiles) {
                console.error(file);
                const arr = this.errorMap[file];
                arr.forEach((v) => console.error(v));
                console.log('---------------------')
            }
            await Alert.Instance.sendBuildFailureAlert(`富文本格式错误: ${errorFiles[0].replaceAll('\\', '/')}`);
            await Alert.Instance.alertErrorFile(errorFiles[0]);
            process.exit(1);
        }

        for (const structName in structJsonMap) {
            const jinfos = structJsonMap[structName];
            for (const info of jinfos) {
                let saveName = this.nameMap?.[info.fb];
                if (saveName == null)
                    saveName = structName + '.json';
                if (structXmlMap[structName].length > 1) {
                    const mch = info.fb.match(/\w+\.(\w+)\.xml/);
                    if (mch != null) {
                        saveName = saveName.replace('.json', `.${mch[1]}.json`);
                    } else {
                        console.error(`Different xmls contain same struct! Cannot decide the json name! ${jinfos.map((value) => value.fb).join(', ')}`);
                        process.exit(1);
                    }    
                }
                await fs.writeJson(path.join(this.jsonPath, saveName), info.cfgs, { spaces: 2 });
            }
        }

        if (this.strings.length > 0) {
            // 生成strings.json
            await fs.writeJSON(path.join(this.jsonPath, 'strings.json'), this.strings, { spaces: 2 });
        }
        if (this.allLangs.length > 0) {
            this.allLangs.sort();
            await fs.writeJSON(path.join(this.jsonPath, 'lang.json'), this.allLangs, { spaces: 2 });
        }

        // 清洗数据
        await this.shakeTree();
        // 生成bundle.json
        if (toolchain.projCfg.xml2json?.bundle == 'YES' || toolchain.projCfg.xml2json?.bundle == 'YES_JS') {
            // 由于旧项目可能会对所有json达成一个ab包，故增加一个新控制开关
            await this.makeBundle();
        }

        // 提取字符串
        if (toolchain.projCfg.xml2json?.extractStrings == 'YES') {
            await this.extractStrings();
        }
    }

    private getStructFingerprint(struct: StructDef): string {
        let s = JSON.stringify(struct);
        for (const e of struct.entries) {
            if (isComplexType(e._attributes.type) && !e.isAdditional) {
                const et = JsonStructParser.Instance.getStructDef(e._attributes.type);
                s += this.getStructFingerprint(et!);
            }
        }
        return s;
    }

    private async checkValidity(structDef: StructDef, cfgs: JsonDef[]) {
        // 检查unique字段，目前只支持非数组字段，要求不同配置的字段值不同
        const entries = structDef.entries.filter(v => v.isUnique);
        for (const entry of entries) {
            if (isArrayEntry(entry)) {
                console.warn(`[WARN]Unique field ${entry._attributes.name} is array entry, cannot check!`);
            } else {
                const valueRecorder: Record<string, number> = {};
                for (const c of cfgs) {
                    const v = c[entry._attributes.name];
                    if (v == null) continue;
                    if (valueRecorder[v as string] > 0) {
                        const ci = JsonStructParser.Instance.getConvInfo(this.crtStruct);
                        await Alert.Instance.sendBuildFailureAlert(`${ci.sheet}@${ci.xlsx}中的列${entry._attributes.name}(${entry._attributes.cname ?? entry._attributes.desc})的值不允许重复！`);
                        process.exit(1);
                    } else {
                        valueRecorder[v as string] = (valueRecorder[v as string] ?? 0) + 1;
                    }
                }
            }
        }
        // 检查validate规则
        const ventryies = structDef.entries.filter(v => v.validates != null);
        for (const entry of ventryies) {
            for (const v of entry.validates!) {
                if (v.type == 'LE') {
                    for (const c of cfgs) {
                        const cv = c[entry._attributes.name] as number;
                        if (cv == null) continue;
                        if (cv > v.value) {
                            const ci = JsonStructParser.Instance.getConvInfo(this.crtStruct);
                            await Alert.Instance.sendBuildFailureAlert(`${ci.sheet}@${ci.xlsx}中的列${entry._attributes.name}(${entry._attributes.cname ?? entry._attributes.desc})的值校验失败！(${v.description})`);
                            process.exit(1);
                        }
                    }
                }
            }
        }
    }

    private async shakeTree(): Promise<void> {
        // 解析清洗规则
        const tsrRules = toolchain.projCfg.xml2json?.treeShakeRules;
        if (!tsrRules) {
            console.log('no treeshake rules, skip!');
            return;
        }
        console.log('treeshake rules:', tsrRules);

        // 清洗数据
        for (const rule of tsrRules) {
            let equalHitValues: NumStr[] = [], notEqualHitValues: NumStr[] = [];
            for (const test of rule.tests) {
                const [equalName, equalKeyStr] = test.select.split('::');
                const equalKeys = equalKeyStr.split('.');
                const equalJsonFile = path.join(this.jsonPath, `${equalName}.json`);
                const equalJson = await fs.readJSON(equalJsonFile) as any[];
                for (const c of equalJson) {
                    const values = getValues(c, equalKeys).filter((v) => Boolean(v));
                    if (test.rule == 'if_equal') {
                        equalHitValues.push(...values);
                    } else {
                        notEqualHitValues.push(...values);
                    }
                }
            }
            equalHitValues = _.uniq(equalHitValues);
            notEqualHitValues = _.uniq(notEqualHitValues);

            if (equalHitValues.length > 0) {
                console.log(`tree shake ${rule.name}, ${equalHitValues.length} item off`);
                const keepKeys = rule.then_keep.split(',');
                const jsonFile = path.join(this.jsonPath, `${rule.name}.json`);
                const json = await fs.readJson(jsonFile) as any[];
                const newCfgs: any[] = [];
                for (const c of json) {
                    if (equalHitValues.includes(c[rule.if_key]) && !notEqualHitValues.includes(c[rule.if_key])) {
                        newCfgs.push(_.pick(c, keepKeys));
                    } else {
                        newCfgs.push(c);
                    }
                }
                await fs.writeJson(jsonFile, newCfgs, { spaces: 2 });
            }
        }
    }

    private dropFields(rule: JSONDropFieldRule, cfgs: any[], structDef: StructDef): void {
        for (const c of cfgs) {
            for (const dk of rule.dropKeys) {
                const entryDef = structDef.entries.find((v) => v._attributes.name == dk.name);
                const f = c[dk.name];
                if (f instanceof Array) {
                    let reduceCnt = 0;
                    for (let i = f.length - 1; i >= 0; i--) {
                        const elem = f[i];
                        if (this.testDropable(elem, dk.tests)) {
                            f.splice(i, 1);
                            reduceCnt++;
                        }
                    }
                    if (reduceCnt > 0) {
                        if (entryDef!._attributes.refer) {
                            c[entryDef!._attributes.refer] = c[entryDef!._attributes.refer] - reduceCnt;
                            // 检查optional字段
                            if (c[entryDef!._attributes.refer] == 0) {
                                const referEntry = structDef.entries.find((v) => v._attributes.name == entryDef?._attributes.refer);
                                if (referEntry!.isOptional) delete c[entryDef!._attributes.refer];
                            }
                        }
                        if (f.length == 0 && entryDef?.isOptional) {
                            delete c[dk.name];
                        }
                    }
                } else {
                    if (this.testDropable(f, dk.tests)) {
                        delete c[dk.name];
                    }
                }
            }
        }
    }

    private testDropable(f: any, tests?: JSONDropFieldRule['dropKeys'][0]['tests']): boolean {
        if (typeof(f) == 'object') {
            // 复杂字段
            if (tests) {
                for (const test of tests) {
                    if (test.hitValue != null) {
                        if (f[test.key] == test.hitValue) {
                            return true;
                        }
                    } else {
                        if (!f[test.key]) {
                            return true;
                        }
                    }
                }
            } else {
                // 没有tests，默认所有字段都是空字段则命中
                let used = false;
                for (const key in f) {
                    if (f[key]) {
                        used = true;
                        break;
                    }
                }
                return !used;
            }
        } else {
            if (tests) {
                // 简单字段可以通过hitValue指定命中值
                for (const test of tests) {
                    if (f == test.hitValue) {
                        return true;
                    }
                }
            } else {
                // 没有tests，默认空字段即命中
                return !f;
            }
        }
        return false;
    }

    private diffValues(rule: JsonDiffValueRule, cfgs: any[], structDef: StructDef): void {
        // 先按照系列进行分类
        const defaultSerial = '_serial';
        const serialMap: { [serial: string]: any[] } = {};
        if (rule.serial_keys) {
            for (const c of cfgs) {
                const svk = joinValues(c, rule.serial_keys);
                let m = serialMap[svk];
                if (!m) serialMap[svk] = m = [];
                m.push(c);
            }
        } else {
            // 否则视为同一系列
            serialMap[defaultSerial] = cfgs.slice();
        }
        // 再进行排序
        for (const serial in serialMap) {
            serialMap[serial] = _.sortBy(serialMap[serial], rule.serial_sort_keys);
        }
        // 开始diff
        for (const svk in serialMap) {
            const m = serialMap[svk];
            for (let i = m.length - 1; i > 0; i--) {
                const prev = m[i - 1];
                const elem = m[i];
                for (const f of rule.diff_fields) {
                    if (prev[f.name] == null) continue;
                    const entryDef = structDef.entries.find((v) => v._attributes.name == f.name)!;
                    if (prev[f.name] instanceof Array) {
                        const arr = [];
                        for (let i = 0, len = elem[f.name].length; i < len; i++) {
                            const p1 = prev[f.name][i];
                            const p2 = elem[f.name][i];
                            if (p1 == null) {
                                arr.push(p2);
                            } else {
                                if (f.key) {
                                    if (p2[f.key] != p1[f.key]) {
                                        p2[f.key] = (p2[f.key] || 0) - (p1[f.key] || 0);
                                        arr.push(p2);
                                    }
                                } else {
                                    if (p2 != p1) {
                                        arr.push((p2 || 0) - (p1 || 0));
                                    }
                                }
                            }
                        }
                        if (arr.length == 0 && entryDef.isOptional) {
                            delete elem[f.name];
                        } else {
                            elem[f.name] = arr;
                        }
                    } else {
                        const p1 = prev[f.name];
                        const p2 = elem[f.name];
                        if (f.key) {
                            p2[f.key] = (p2[f.key] || 0) - (p1[f.key] || 0);
                            if (p2[f.key] == 0 && entryDef.isOptional) {
                                delete elem[f.name];
                            }
                        } else {
                            const newValue = (p2 || 0) - (p1 || 0);
                            if (newValue == 0 && entryDef.isOptional) {
                                delete elem[f.name];
                            } else {
                                elem[f.name] = newValue;
                            }
                        }
                    }
                }
            }
        }
    }

    private async makeBundle(): Promise<void> {
        const jsons = await fg('*.json', { cwd: this.jsonPath, ignore: ['lang.json', 'bundle.json', 'dataVer.json'] });
        // 构建脚本的JsonTerser会在对json进行混淆时重新覆盖生成bundle.json，此处需注意保持文件格式统一
        const bundle: Record<string, any[]> = {};
        for (const j of jsons) {
            const jfile = path.join(this.jsonPath, j);
            const jsonContent = await fs.readFile(jfile, 'utf-8');
            bundle[path.basename(j, '.json')] = JSON.parse(jsonContent);
        }
        if (toolchain.projCfg.xml2json?.bundle == 'YES_JS') {
            await this.json2js(path.join(this.jsonPath, 'bundle.js'), bundle);
        } else {
            await fs.writeJSON(path.join(this.jsonPath, 'bundle.json'), bundle);
        }
    }

    private async structToJson(obj: CfgLineDef | ComplexAttrDef, structDef: StructDef, onlyForLang: boolean): Promise<JsonOut> {
        let isNotUsed = true;
        let o: any = {}, deleteKeys: string[] = [];
        for(let p of structDef.entries) {
            if (p._attributes.name === name_ClientUsed) continue;
            const noJson = onlyForLang || p.isNoJson, lang = onlyForLang || p.isLang, optional = !onlyForLang && p.isOptional;

            let so = obj[p._attributes.name];
            if(!so) {
                // 表格里没填，则xml不会转出相应字段
                if(!p.isAdditional && !noJson) {
                    o[p._attributes.name] = this.getDefaultValue(p, false);
                    if (optional) {
                        deleteKeys.push(p._attributes.name);
                    }
                }
            } else {
                if(isArrayEntry(p)) {
                    // Array
                    let pArr: JsonDef[] = [];
                    let emptyAllowed = false;
    
                    if(!(so instanceof Array)) {
                        // 数值型数组会转成以空格隔开的单个字段
                        if(!isComplexType(p._attributes.type)) {
                            console.assert(typeof(so._text) == 'string');
                            // 逻辑上说，只有p._attributes.sparse_array == '1'的情况下emptyAllowed才为true
                            // trimArrayEnd讲自动裁剪掉无效的数组，为了兼容老项目，需显示启用本功能
                            emptyAllowed = toolchain.projCfg.xml2json?.trimArrayEnd == 'YES' ? p._attributes.sparse_array == '1' : true;
                            // todo: 是否应该trim()，为了维持和perl版结果一致，NPCRandomStoreCfgM:m_dtRefreshTimes会多转出一个空字符串
                            let textArr = (<string>so._text).trimEnd().split(/\s+/);
                            let newSoArr: AttrDef[] = [];
                            for(let textElem of textArr) {
                                newSoArr.push({ _text: textElem });
                            }
                            so = newSoArr;
                        }
                    }

                    let entryNotUsed = false;
                    if(so instanceof Array) {
                        let soArr = so as AttrDef[] | ComplexAttrDef[];
                        if(!soArr) {
                            console.error(`array undefined!`);
                        }
                        let emptyElems: JsonDef[] = [], usedCnt = 0;
                        for(let soElem of soArr) {
                            let sout = await this.propertyToJson(<AttrDef | ComplexAttrDef>soElem, p, true, onlyForLang);
                            if (!sout.isNotUsed) usedCnt++;
                            if(!sout.isNotUsed || emptyAllowed) {  
                                // 先将先前的空结构压入
                                for(let ee of emptyElems) {
                                    pArr.push(ee);
                                }
                                emptyElems.length = 0;
                                pArr.push(sout.o);
                            } else {
                                if (toolchain.projCfg.xml2json?.trimArrayEnd == 'YES' || p._attributes.sparse_array == '1') {
                                    // 逻辑上说无论如何都需要缓存起来
                                    // 为了兼容老项目（老项目需p._attributes.sparse_array == '1'），有了这个if
                                    // 先将空结构缓存起来
                                    emptyElems.push(sout.o);
                                }
                            }
                        }
                        entryNotUsed = usedCnt == 0;
                    } else {
                        // Although defined as array, but only one element got
                        const sout = await this.propertyToJson(<AttrDef | ComplexAttrDef>so, p, true, onlyForLang);
                        entryNotUsed = sout.isNotUsed;
                        if(!sout.isNotUsed) {
                            pArr.push(sout.o);
                        }
                    }
    
                    if (!noJson) {
                        o[p._attributes.name] = pArr;
                        if (entryNotUsed && optional) {
                            deleteKeys.push(p._attributes.name);
                        }
                    }
                    if (!entryNotUsed) {
                        isNotUsed = false;
                    }
                } else {
                    // Not array
                    console.assert(!(so instanceof Array), `Single node expected: ${p._attributes.name}`);
                    const sout = await this.propertyToJson(<AttrDef | ComplexAttrDef>so, p, false, onlyForLang);
                    if (!noJson) {
                        o[p._attributes.name] = sout.o;
                        if (sout.isNotUsed && optional) {
                            deleteKeys.push(p._attributes.name);
                        }
                        if(!sout.isNotUsed) {
                            isNotUsed = false;
                        }
                    }
                    if (isLiteralStringType(p._attributes.type) && typeof(sout.o) === 'string') {
                        if (sout.o && lang && !this.allLangs.includes(sout.o) && sout.o.search(this.HanPattern) >= 0) this.allLangs.push(sout.o);
                        if (p.isAtoiArr) {
                            if (sout.isNotUsed) {
                                if (!optional) {
                                    o[p._attributes.name] = [];
                                }
                            } else {
                                o[p._attributes.name] = sout.o.split(/[,，]/).map((v) => parseInt(v));
                            }
                        } else if (p.isAtoi && !sout.isNotUsed) {
                            const idx = this.strings.indexOf(sout.o);
                            if (idx >= 0) {
                                o[p._attributes.name] = idx;
                            } else {
                                this.strings.push(sout.o);
                                o[p._attributes.name] = this.strings.length - 1;
                            }
                        }
                    }
                }
            }
        }
        this.toDeletes.push({ o, deleteKeys });
        return { o, isNotUsed: isNotUsed };
    }

    /**
     * Convert a property into json, a 'true' value will be returned when the value is significant.
     * @param obj 
     * @param out 
     * @param p 
     * @returns 
     */
    private async propertyToJson(obj: AttrDef | ComplexAttrDef, p: EntryDef, isArrayItem: boolean, onlyForLang: boolean): Promise<JsonOut> {
        if(isComplexType(p._attributes.type)) {
            // Complex attr
            let complexObj = obj as ComplexAttrDef;
            complexObj._attributes.type = complexObj._attributes.type.replace('_Flash', 'M');
            if(p._attributes.type != complexObj._attributes.type) {
                // 可能结构定义已改，但是未转表，导致字段类型不匹配
                console.error(`[ERROR]Property type not match, ${p._attributes.type}(bin) vs ${complexObj._attributes.type}(definition): ${this.crtXml}`);
                const convInfo = JsonStructParser.Instance.getConvInfo(this.crtStruct);
                await Alert.Instance.sendBuildFailureAlert(`表格未转表导致转json失败: ${convInfo.name}(${convInfo.sheet}@${convInfo.xlsx})`);
                process.exit(1);
            }
            let pStructDef = JsonStructParser.Instance.getStructDef(p._attributes.type);
            if(!pStructDef) {
                console.error(`[ERROR]Cannot find struct definition: ${p._attributes.type}`);
                process.exit(1);
            }
            return await this.structToJson(complexObj, pStructDef, onlyForLang);
        } 
        // Simple attr
        if((<AttrDef>obj)._text) {
            let o = this.convertValue((<AttrDef>obj)._text, p);
            if (isLiteralStringType(p._attributes.type) && typeof(o) === 'string' && o.startsWith('0')) {
                return { o, isNotUsed: true };
            }
            if (Boolean(o)) {
                this.crtWeight++;
            }
            return { o, isNotUsed: !Boolean(o) };
        } else {
            let o = this.getDefaultValue(p, isArrayItem);
            return { o, isNotUsed: true };
        }
    }

    private convertValue(raw: string, p: EntryDef): number | string {
        if(StringLike.includes(p._attributes.type)) {
            // 换行符换成\n
            let out = raw.replace(/(?:\\r)?\\n/g, '\n').replace(/\r?\n/g, '\n');
            // 检查富文本格式
            if (count(out.replaceAll('#N', '').replaceAll(/<color=#\w.+>/g, '').replaceAll(/<font color=('|")#\w.+\1>/g, ''), '#') % 2 != 0) {
                let arr = this.errorMap[this.crtXml];
                if (arr == null) this.errorMap[this.crtXml] = arr = [];
                arr.push(out);
            }
            return out;
        }
        // 将字符串转成数值
        let num = Number(raw);
        if(num == -2147483648) {
            console.error(`[ERROR]Invalid value ${raw} found in ${this.crtXml}`);
            process.exit(1);
        }
        return num;
    }

    private getDefaultValue(p: EntryDef, isArrayItem: boolean): 0 | '' | [] | {} {
        if(!isArrayItem && isArrayEntry(p)) {
            return [];
        }
        if(NumberLike.includes(p._attributes.type)) {
            return 0;
        }
        if(StringLike.includes(p._attributes.type)) {
            return '';
        }
        return {};
    }

    private identifyStructNameKey(doc: CfgXmlSet): string | null {
        for(let key in doc) {
            if(key.endsWith('_Flash_Tab')) {
                return key;
            }
        }
        return null;
    }

    private extractStrDirty = false;
    private async extractStrings(): Promise<void> {
        const strJson = path.join(this.jsonPath, 'strings.json');
        const jsons = await fg('*.json', { cwd: this.jsonPath, ignore: ['LangCfgM.json', 'langs.json', 'strings.json'] });
        const strings: string[] = [], strMap: Record<string, number> = {};
        for (const js of jsons) {
            const jsf = path.join(this.jsonPath, js);
            const cfgs = await fs.readJson(jsf);
            this.extractStrDirty = false;
            this.collectStrings(cfgs, strings, strMap);
            if (this.extractStrDirty) {
                await fs.writeJson(jsf, cfgs, { spaces: 2 });
            }
        }
        await fs.writeJSON(strJson, strings, { spaces: 2 });
    }

    private collectStrings(o: any, strings: string[], strMap: Record<string, number>): void {
        for (const key in o) {
            const value = o[key];
            const t = typeof(value);
            if (t == 'string') {
                let strId = strMap[value];
                if (!strId) {
                    strings.push(value);
                    strId = strings.length;
                    strMap[value] = strId;
                }
                o[key] = strId;
                this.extractStrDirty = true;
            } else if (t == 'object') {
                this.collectStrings(value, strings, strMap);                    
            }
        }
    }

    private async json2js(jsf: string, o: any): Promise<void> {
        const strings: string[] = [];
        this.collectConsts(o, strings);
        const cfgsStr = JSON.stringify(o, undefined, 2).replaceAll(/(?<=: )"\$s(\d+)"/g, (substring: string, ...args: any[]) => `_ss[${args[0]}]`).replaceAll(/"(\w+)"(?=: )/g, (stubString: string, ...args: any[]) => args[0]);
        let content = `const _ss = ${JSON.stringify(strings, undefined, 2)};
window._cfgs = ${cfgsStr};
`;
        await fs.writeFile(jsf, content, 'utf-8');
    }

    private collectConsts(o: any, strings: string[]): void {
        for (const key in o) {
            const value = o[key];
            const t = typeof(value);
            if (t == 'string') {
                let idx = strings.indexOf(value);
                if (idx < 0) {
                    strings.push(value);
                    idx = strings.length - 1;
                }
                o[key] = `$s${idx}`;
            } else if (t == 'object') {
                this.collectConsts(value, strings);
            }
        }
    }

    private async makeErrorId(): Promise<void> {
        let fileContent = await fs.readFile(path.join(this.xmlPath, 'Errno.config.xml'), 'utf-8');

        console.log('making ErrorId.ts...');

        let xml = xmljs.xml2json(fileContent, {compact: true, spaces: 0});
        let doc = JSON.parse(xml) as CfgXmlSet;
        let items = doc['ErrnoConfig_Flash_Tab']['ErrnoConfig_Flash'];
        let itemArr: CfgLineDef[];
        if(items instanceof Array) {
            itemArr = items;
        } else {
            itemArr = [items];
        }

        let content = '/**错误码集合(由xml2json自动生成)*/\n';
        content += `export const enum ErrorId {\n`
        for(let item of itemArr) {
            let name = item['m_szName'] as AttrDef;
            if (name._text.startsWith('0')) continue;
            let desc = item['m_szDescriptionZH'] as AttrDef;
            let value = item['m_uiValue'] as AttrDef;
            content += `    /**${desc._text}*/\n`;
            content += `    ${name._text} = ${value._text},\n`;
        }
        content += '}';

        await fs.writeFile(path.join(this.errorIdPath, 'ErrorId.ts'), content, 'utf-8');
    }
}